/**
 * File: GZIPAppendInputStream.java
 * Created by: mhaimel
 * Created on: 15 Oct 2009
 * CVS:  $Id: GZIPAppendInputStream.java,v 1.1 2009/11/24 15:11:54 mhaimel Exp $
 */
package uk.ac.ebi.curtain.utils.io;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

/**
 * 
 * @author mhaimel
 *
 */
public class GZIPAppendInputStream extends InflaterInputStream {
    /**
     * CRC-32 for uncompressed data.
     */
    protected CRC32 crc = new CRC32();

    /**
     * Indicates end of input stream.
     */
    protected boolean eos;

    private boolean closed = false;
    
    /**
     * Check to make sure that this stream has not been closed
     */
    private void ensureOpen() throws IOException {
	if (closed) {
	    throw new IOException("Stream closed");
        }
    }

    /**
     * Creates a new input stream with the specified buffer size.
     * @param in the input stream
     * @param size the input buffer size
     * @exception IOException if an I/O error has occurred
     * @exception IllegalArgumentException if size is <= 0
     */
    public GZIPAppendInputStream(InputStream in, int size) throws IOException {
	super(in, new Inflater(true), size);
//        usesDefaultInflater = true; // just for closing inflater
	readHeader();
	crc.reset();
    }

    /**
     * Creates a new input stream with a default buffer size.
     * @param in the input stream
     * @exception IOException if an I/O error has occurred
     */
    public GZIPAppendInputStream(InputStream in) throws IOException {
	this(in, 512);
    }
    /**
     * Reads uncompressed data into an array of bytes. If <code>len</code> is not
     * zero, the method will block until some input can be decompressed; otherwise,
     * no bytes are read and <code>0</code> is returned.
     * @param buf the buffer into which the data is read
     * @param off the start offset in the destination array <code>b</code>
     * @param len the maximum number of bytes read
     * @return	the actual number of bytes read, or -1 if the end of the
     *		compressed input stream is reached
     * @exception  NullPointerException If <code>buf</code> is <code>null</code>.
     * @exception  IndexOutOfBoundsException If <code>off</code> is negative, 
     * <code>len</code> is negative, or <code>len</code> is greater than 
     * <code>buf.length - off</code>
     * @exception IOException if an I/O error has occurred or the compressed
     *			      input data is corrupt
     */
    public int read(byte[] buf, int off, int len) throws IOException {
        ensureOpen();
	if (eos) {
	    return -1;
	}
	int retLen = super.read(buf, off, len);
	if (retLen == -1) {
	    readTrailer();
	    if(readHeader()){
	    	crc.reset();
	    	inf.reset();
//	    	inf = new Inflater(true);
//	    	fill();
	    	retLen = super.read(buf, off, len);
	    } else {
	    	eos = true;
	    }
	} 
	if(retLen != -1){
	    crc.update(buf, off, retLen);
	}
	return retLen;
    }
    
//    /**
//     * Fills input buffer with more data to decompress.
//     * @exception IOException if an I/O error has occurred
//     */
//    protected void fill() throws IOException {
//	ensureOpen();
//	len = in.read(buf, 0, buf.length);
//	if (len == -1) {
//	    throw new EOFException("Unexpected end of ZLIB input stream");
//	}
//	inf.setInput(buf, 0, len);
//    }

    /**
     * Closes this input stream and releases any system resources associated
     * with the stream.
     * @exception IOException if an I/O error has occurred
     */
    public void close() throws IOException {
        if (!closed) {
            super.close();
            this.inf.end();
            eos = true;
            closed = true;
        }
    }

    /**
     * GZIP header magic number.
     */
    public final static int GZIP_MAGIC = 0x8b1f;

    /*
     * File header flags.
     */
    private final static int FTEXT	= 1;	// Extra text
    private final static int FHCRC	= 2;	// Header CRC
    private final static int FEXTRA	= 4;	// Extra field
    private final static int FNAME	= 8;	// File name
    private final static int FCOMMENT	= 16;	// File comment

    /*
     * Reads GZIP member header.
     */
    private boolean readHeader() throws IOException {
    	byte[] arr = new byte[512]; // header + trailer length
    	int read = this.in.read(arr);
    	boolean valid = false;
    	if(read > 10){
    		ByteArrayInputStream bin = new ByteArrayInputStream(arr, 0,read);
    		IOException exc = readHeader(bin);
    		if(null != exc){
    			throw exc;
    		} else {
    			valid = true;
        		if(bin.available() > 0){
        			this.in = new SequenceInputStream(bin,this.in);
        		}
    		}
    	}
    	return valid;
    }
    
    /*
     * Reads GZIP member header.
     */
    private IOException readHeader(ByteArrayInputStream bin) throws IOException {
    	CheckedInputStream in = new CheckedInputStream(bin, crc);
    	crc.reset();
    	// Check header magic
    	if (readUShort(in) != GZIP_MAGIC) {
    	    return new IOException("Not in GZIP format");
    	}
    	// Check compression method
    	if (readUByte(in) != 8) {
    		return new IOException("Unsupported compression method");
    	}
    	// Read flags
    	int flg = readUByte(in);
    	// Skip MTIME, XFL, and OS fields
    	skipBytes(in, 6);
    	// Skip optional extra field
    	if ((flg & FEXTRA) == FEXTRA) {
    	    skipBytes(in, readUShort(in));
    	}
    	// Skip optional file name
    	if ((flg & FNAME) == FNAME) {
    	    while (readUByte(in) != 0) ;
    	}
    	// Skip optional file comment
    	if ((flg & FCOMMENT) == FCOMMENT) {
    	    while (readUByte(in) != 0) ;
    	}
    	// Check optional header CRC
    	if ((flg & FHCRC) == FHCRC) {
    	    int v = (int)crc.getValue() & 0xffff;
    	    if (readUShort(in) != v) {
    	    	return new IOException("Corrupt GZIP header");
    	    }
    	}
    	return null;
    }

    /*
     * Reads GZIP member trailer.
     */
    private void readTrailer() throws IOException {
	InputStream in = this.in;
	int n = inf.getRemaining();
	if (n > 0) {
	    in = new SequenceInputStream(
			new ByteArrayInputStream(buf, len - n, n), in);
	}
	// Uses left-to-right evaluation order
	long readUInt = readUInt(in);
	long readUInt2 = readUInt(in);
	if ((readUInt != crc.getValue()) ||
	    // rfc1952; ISIZE is the input size modulo 2^32
	    (readUInt2 != (inf.getBytesWritten() & 0xffffffffL)))
	    throw new IOException("Corrupt GZIP trailer");
	this.in = in;
    }

    /*
     * Reads unsigned integer in Intel byte order.
     */
    private long readUInt(InputStream in) throws IOException {
	long s = readUShort(in);
	return ((long)readUShort(in) << 16) | s;
    }

    /*
     * Reads unsigned short in Intel byte order.
     */
    private int readUShort(InputStream in) throws IOException {
	int b = readUByte(in);
	return ((int)readUByte(in) << 8) | b;
    }

    /*
     * Reads unsigned byte.
     */
    private int readUByte(InputStream in) throws IOException {
	int b = in.read();
	if (b == -1) {
	    throw new EOFException();
	}
        if (b < -1 || b > 255) {
            // Report on this.in, not argument in; see read{Header, Trailer}.
            throw new IOException(this.in.getClass().getName()
                + ".read() returned value out of range -1..255: " + b);
        }
	return b;
    }


    private byte[] tmpbuf = new byte[128];

    /*
     * Skips bytes of input data blocking until all bytes are skipped.
     * Does not assume that the input stream is capable of seeking.
     */
    private void skipBytes(InputStream in, int n) throws IOException {
	while (n > 0) {
	    int len = in.read(tmpbuf, 0, n < tmpbuf.length ? n : tmpbuf.length);
	    if (len == -1) {
		throw new EOFException();
	    }
	    n -= len;
	}
    }
}
